#region Copyright Notice
//
// Copyright (c) Microsoft Corporation.  All rights reserved.
// Scope Compiler Patent Pending 
//
#endregion

namespace Microsoft.ServiceModel.Samples.Discovery
{
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Globalization;

    internal class ScopeCompiler
    {
        public static readonly Uri MatchByRfc2396 =
            new Uri("http://schemas.xmlsoap.org/ws/2005/04/discovery/rfc2396");

        public static readonly Uri MatchByUuid =
            new Uri("http://schemas.xmlsoap.org/ws/2005/04/discovery/uuid");

        public static readonly Uri MatchByLdap =
            new Uri("http://schemas.xmlsoap.org/ws/2005/04/discovery/ldap");

        public static readonly Uri MatchByStrcmp0 =
            new Uri("http://schemas.xmlsoap.org/ws/2005/04/discovery/strcmp0");

        public static string[] Compile(IEnumerable<Uri> scopes)
        {
            List<string> compiledScopes = new List<string>();

            foreach (Uri scope in scopes)
                Compile(scope, compiledScopes);
            
            return compiledScopes.ToArray();
        }

        static void Compile(Uri scope, List<string> compiledScopes)
        {         
            // MatchByRfc2396 can be applied to any URI
            compiledScopes.Add(CompileForMatchByRfc2396(scope));

            // MatchByUuid can be applied to only UUIDs
            if (string.Compare(scope.Scheme, "uuid", StringComparison.OrdinalIgnoreCase) == 0)
            {
                compiledScopes.Add(CompileForMatchByUuid(scope));
            }

            // MatchByStrcmp0 can be applied to any URI
            compiledScopes.Add(CompileForMatchByStrcmp0(scope));

            // MatchByLdap can be applied to only LDAP URI
            if (string.Compare(scope.Scheme, "ldap", StringComparison.OrdinalIgnoreCase) == 0)
            {
                compiledScopes.Add(CompileForMatchByLdap(scope));
            }
        }

        public static CompiledCriteria CompileCriteria(Uri scope, Uri matchBy)
        {
            CompiledCriteria criteria = new CompiledCriteria();

            if (matchBy.Equals(MatchByRfc2396))
            {
                criteria.exactMatch = false;
                criteria.criteriaString = CompileForMatchByRfc2396(scope);
                return criteria;
            }

            if (matchBy.Equals(MatchByUuid))
            {
                criteria.exactMatch = true;
                criteria.criteriaString = CompileForMatchByUuid(scope);
                return criteria;
            }

            if (matchBy.Equals(MatchByLdap))
            {
                criteria.exactMatch = false;
                criteria.criteriaString = CompileForMatchByLdap(scope);
                return criteria;
            }

            if (matchBy.Equals(MatchByStrcmp0))
            {
                criteria.exactMatch = true;
                criteria.criteriaString = CompileForMatchByStrcmp0(scope);
                return criteria;
            }

            throw new ArgumentOutOfRangeException("matchBy",
                "Matching rule " + matchBy.ToString() + " is not supported.");
        }

        public static bool IsMatch(string[] compiledScopes, Uri searchScope, 
            Uri matchBy)
        {
            CompiledCriteria compiledCriteria = 
                CompileCriteria(searchScope, matchBy);

            bool isMatch = false;
            for (int i = 0; i < compiledScopes.Length; i++)
            {
                if (compiledCriteria.exactMatch)
                {
                    isMatch = (string.Compare(compiledScopes[i],
                        compiledCriteria.criteriaString, StringComparison.Ordinal) == 0);
                }
                else
                {
                    isMatch = compiledScopes[i].StartsWith(
                        compiledCriteria.criteriaString);
                }

                if (isMatch) break;
            }

            return isMatch;
        }

        static string CompileForMatchByRfc2396(Uri scope)
        {
            StringBuilder compiledScopeBuilder = new StringBuilder();

            // Append the matching rule name, so this compiled scope can only be 
            // matched for that particular matching rule.
            compiledScopeBuilder.Append("rfc2396match::");

            //
            // Rule: Using a case-insensitive comparison, The scheme [RFC 2396] 
            //       of S1 and S2 is the same and
            //
            string scheme = scope.GetComponents(UriComponents.Scheme,
                UriFormat.UriEscaped);
            if (scheme != null)
                scheme = scheme.ToLower(CultureInfo.InvariantCulture);
            else
                scheme = string.Empty;
            compiledScopeBuilder.Append(scheme);
            compiledScopeBuilder.Append(":");

            // 
            // Rule: Using a case-insensitive comparison, The authority of S1 
            //       and S2 is the same and
            // 
            string authority = scope.GetComponents(UriComponents.StrongAuthority,
                UriFormat.UriEscaped);
            if (authority != null)
                authority = authority.ToLower(CultureInfo.InvariantCulture);
            else
                authority = string.Empty;
            compiledScopeBuilder.Append(authority);
            compiledScopeBuilder.Append(":");

            // 
            // Rule: The path_segments of S1 is a segment-wise (not string) 
            //       prefix of the path_segments of S2 and Neither S1 nor S2 
            //       contain the "." segment or the ".." segment. All other 
            //       components (e.g., query and fragment) are explicitly 
            //       excluded from comparison. S1 and S2 MUST be canonicalized
            //       (e.g., unescaping escaped characters) before using this 
            //       matching rule.
            foreach (string segment in scope.Segments)
                compiledScopeBuilder.Append(ProcessUriSegment(segment));

            return compiledScopeBuilder.ToString();
        }

        static string ProcessUriSegment(string segment)
        {
            // ignore the segment parameters, if any
            int index = segment.IndexOf(';');
            if (index != -1)
                segment = segment.Substring(0, index);

            // the segment must not contain '.' or '..'
            if ((segment.IndexOf(".") != -1) || (segment.IndexOf("..") != -1))
                throw new ArgumentOutOfRangeException("segment",
                    "The scope segment can not contain '.' or '..'");

            // prevent the comparision of partial segments
            // Note: this matching rule does NOT test whether the string 
            // representation of S1 is a prefix of the string representation 
            // of S2. For example, "http://example.com/abc" matches 
            // "http://example.com/abc/def" using this rule but 
            // "http://example.com/a" does not.
            if (!segment.EndsWith("/"))
                segment = segment + "/";

            return segment;
        }

        static string CompileForMatchByUuid(Uri scope)
        {
            if (string.Compare(scope.Scheme, "uuid", StringComparison.OrdinalIgnoreCase) != 0)
                throw new ArgumentOutOfRangeException("scope",
                    "UUID matching rule can be applied to only UUID URIs");

            // parse the Guid from the URI
            string guidString = scope.GetComponents(UriComponents.Path,
                UriFormat.UriEscaped);
            Guid guid = new Guid(guidString);

            // Append the matching rule name, so this compiled scope can only be 
            // matched for that particular matching rule.
            // 
            // Rule: Using a case-insensitive comparison, the scheme of S1 and 
            //      S2 is "uuid" and each of the unsigned integer fields [UUID]
            //      in S1 is equal to the corresponding field in S2, or 
            //      equivalently, the 128 bits of the in-memory representation 
            //      of S1 and S2 are the same 128 bit unsigned integer.
            return "uuidmatch::" + guid.ToString();
        }

        static string CompileForMatchByStrcmp0(Uri scope)
        {
            // 
            // Rule: Using a case-sensitive comparison, the string 
            //      representation of S1 and S2 is the same.
            //
            return "strcmp0match::" + scope.OriginalString;
        }

        static string CompileForMatchByLdap(Uri scope)
        {
            StringBuilder compiledScopeBuilder = new StringBuilder();

            // Append the matching rule name, so this compiled scope can only be 
            // matched for that particular matching rule.
            compiledScopeBuilder.Append("ldapmatch::");

            //
            // Rule: Using a case-insensitive comparison, the scheme of S1 
            // and S2 is "ldap" and
            string scheme = scope.GetComponents(UriComponents.Scheme,
                UriFormat.UriEscaped);
            if (string.Compare(scheme, "ldap", StringComparison.OrdinalIgnoreCase) != 0)
                throw new ArgumentOutOfRangeException("scope",
                    "LDAP matching rule can be applied to only ldap URIs");
            scheme = scheme.ToLower(CultureInfo.InvariantCulture);
            compiledScopeBuilder.Append(scheme);
            compiledScopeBuilder.Append(":");

            //
            // Rule: and the hostport [RFC 2255] of S1 and S2 is the 
            //       same and
            //
            string hostport = scope.GetComponents(UriComponents.HostAndPort,
                UriFormat.UriEscaped);
            if (hostport != null)
                hostport = hostport.ToLower(CultureInfo.InvariantCulture);
            else
                hostport = string.Empty;
            compiledScopeBuilder.Append(hostport);
            compiledScopeBuilder.Append(":");


            //
            // Rule: and the RDNSequence [RFC 2253] of the dn of S1 is a 
            //       prefix of the RDNSequence of the dn of S2, where comparison
            //       does not support the variants in an RDNSequence described 
            //       in Section 4 of RFC 2253 [RFC 2253].
            //
            // get the ldap DN string.
            string dn = scope.GetComponents(UriComponents.Path,
                UriFormat.Unescaped);

            // parse the RDNs in order from DN
            compiledScopeBuilder.Append(ParseLdapRDNSequence(dn));

            return compiledScopeBuilder.ToString();
        }

        static string ParseLdapRDNSequence(string dn)
        {
            // Assuming the conversion of DN to string as per Section 2 RFC2253
            // ignoring the variations described in section 4.

            StringBuilder rdnSequenceBuilder = new StringBuilder();
            string[] tokens = dn.Split(',');
            StringBuilder rdnBuilder = new StringBuilder();
            foreach (string tok in tokens)
            {
                if (string.IsNullOrEmpty(tok.Trim())) continue;

                if (tok.EndsWith("\\"))
                {
                    // it is part of the RDN
                    rdnBuilder.Append(tok.Substring(0, tok.Length - 1));
                    rdnBuilder.Append(',');
                }
                else
                {
                    // RDN ends here.
                    rdnBuilder.Append(tok);
                    rdnSequenceBuilder.Insert(0, "/");
                    rdnSequenceBuilder.Insert(0,
                        ParseAndSortRDNAttributes(rdnBuilder.ToString()));
                    rdnBuilder = new StringBuilder();
                }
            }

            return rdnSequenceBuilder.ToString();
        }

        static string ParseAndSortRDNAttributes(string rdn)
        {
            //
            // Rule: RFC2253 Section 2: 
            //       When converting from an ASN.1 RelativeDistinguishedName 
            //       to a string, the output consists of the string encodings
            //       of each AttributeTypeAndValue (according to 2.3), in any
            //       order. Where there is a multi-valued RDN, the outputs 
            //       from adjoining AttributeTypeAndValues are separated by 
            //       a plus ('+' ASCII 43) character.
            // 

            // since the RDN attributes can be converted to string in any order
            // we must make sure that the compiled form of the scope and match
            // criteria have the same order so that simple string prefix 
            // comparision produces the same result of comparing the RDN 
            // attribute values individually, we sort the attributes or RDN
            // based on their name.

            // optimize the case where there is only one attrvalue for RDN
            if (rdn.IndexOf('+') == -1) return rdn;

            string[] tokens = rdn.Split('+');
            StringBuilder attrTypeAndValueBuilder = new StringBuilder();
            Dictionary<string, string> attrTypeValueTable =
                new Dictionary<string, string>();
            List<string> attrTypeList = new List<string>();

            foreach (string tok in tokens)
            {
                if (string.IsNullOrEmpty(tok.Trim())) continue;

                if (tok.EndsWith("\\"))
                {
                    // it is part of the attribute value
                    attrTypeAndValueBuilder.Append(
                        tok.Substring(0, tok.Length - 1));
                    attrTypeAndValueBuilder.Append('+');
                }
                else
                {
                    // attribute value ends here.
                    attrTypeAndValueBuilder.Append(tok);

                    // get attribute and value.
                    string attrTypeAndValue = 
                        attrTypeAndValueBuilder.ToString();

                    int equalIdx = attrTypeAndValue.IndexOf('=');
                    if (equalIdx == -1)
                    {
                        throw new ArgumentException(
                            "The AttributeTypeAndValue component of " +
                            "RelativeDistinguishedName is not valid");
                    }

                    string attrType = attrTypeAndValue.Substring(0, equalIdx);
                    string attrValue = attrTypeAndValue.Substring(equalIdx + 1);

                    attrTypeValueTable.Add(attrType, attrValue);
                    attrTypeList.Add(attrType);
                    attrTypeAndValueBuilder = new StringBuilder();
                }
            }

            // sort the list based on the attribute type
            attrTypeList.Sort();

            // created the RDN from the sorted attribute values.
            StringBuilder rdnBuilder = new StringBuilder();
            for (int i = 0; i < attrTypeList.Count - 1; i++)
            {
                rdnBuilder.Append(attrTypeList[i]);
                rdnBuilder.Append("=");
                rdnBuilder.Append(attrTypeValueTable[attrTypeList[i]]);
                rdnBuilder.Append("+");
            }

            if (attrTypeList.Count > 1)
            {
                rdnBuilder.Append(attrTypeList[attrTypeList.Count - 1]);
                rdnBuilder.Append("=");
                rdnBuilder.Append(attrTypeValueTable[
                    attrTypeList[attrTypeList.Count - 1]]);
            }

            return rdnBuilder.ToString();
        }

        public class CompiledCriteria
        {
            public bool exactMatch;
            public string criteriaString;
        }
    }
}
